Completed
Push — master ( d25395...d83c14 )
by Jan
15s queued 14s
created

index.ts ➔ writeSync   A

Complexity

Conditions 2

Size

Total Lines 9
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 9
c 0
b 0
f 0
rs 9.95
cc 2
1
import * as fs from 'fs'
2
import * as ID3Util from './src/ID3Util'
3
import * as TagsHelpers from './src/TagsHelpers'
4
import { isFunction, isString } from './src/util'
5
import { Tags, RawTags, WriteTags } from './src/types/Tags'
6
import { Options } from './src/types/Options'
7
import { updateTags } from './src/update'
8
9
export { Tags, RawTags, WriteTags } from "./src/types/Tags"
10
export { Options } from "./src/types/Options"
11
export { TagConstants } from './src/definitions/TagConstants'
12
13
// Used specification: http://id3.org/id3v2.3.0
14
15
export type WriteCallback = {
16
    (error: null, data: Buffer): void
17
    (error: NodeJS.ErrnoException | Error, data: null): void
18
}
19
20
export type ReadCallback = {
21
    (error: NodeJS.ErrnoException | Error, tags: null): void
22
    (error: null, tags: Tags | RawTags): void
23
}
24
25
export type RemoveCallback =
26
    (error: NodeJS.ErrnoException | Error | null) => void
27
28
export type CreateCallback =
29
    (data: Buffer) => void
30
31
/**
32
 * Remove already written ID3-Frames from a buffer
33
 */
34
export function removeTagsFromBuffer(data: Buffer) {
35
    const tagPosition = ID3Util.getTagPosition(data)
36
37
    if (tagPosition === -1) {
38
        return data
39
    }
40
41
    const tagHeaderSize = 10
42
    const encodedSize = data.subarray(
43
        tagPosition + 6,
44
        tagPosition + tagHeaderSize
45
    )
46
    if (!ID3Util.isValidEncodedSize(encodedSize)) {
47
        return false
48
    }
49
50
    if (data.length >= tagPosition + tagHeaderSize) {
51
        const size = ID3Util.decodeSize(encodedSize)
52
        return Buffer.concat([
53
            data.subarray(0, tagPosition),
54
            data.subarray(tagPosition + size + tagHeaderSize)
55
        ])
56
    }
57
58
    return data
59
}
60
61
function writeInBuffer(tags: Buffer, buffer: Buffer) {
62
    buffer = removeTagsFromBuffer(buffer) || buffer
63
    return Buffer.concat([tags, buffer])
64
}
65
66
function writeAsync(tags: Buffer, filepath: string, callback: WriteCallback) {
67
    fs.readFile(filepath, (error, data) => {
68
        if(error) {
69
            callback(error, null)
70
            return
71
        }
72
        const newData = writeInBuffer(tags, data)
73
        fs.writeFile(filepath, newData, 'binary', (error) => {
74
            if (error) {
75
                callback(error, null)
76
            } else {
77
                callback(null, newData)
78
            }
79
        })
80
    })
81
}
82
83
function writeSync(tags: Buffer, filepath: string) {
84
    try {
85
        const data = fs.readFileSync(filepath)
86
        const newData = writeInBuffer(tags, data)
87
        fs.writeFileSync(filepath, newData, 'binary')
88
        return true
89
    } catch(error) {
90
        return error as Error
91
    }
92
}
93
94
/**
95
 * Write passed tags to a file/buffer
96
 */
97
export function write(tags: WriteTags, buffer: Buffer): Buffer
98
export function write(tags: WriteTags, filepath: string): true | Error
99
export function write(
100
    tags: WriteTags, filebuffer: string | Buffer, callback: WriteCallback
101
): void
102
export function write(
103
    tags: WriteTags,
104
    filebuffer: string | Buffer,
105
    callback?: WriteCallback
106
): Buffer | true | Error | void {
107
    const tagsBuffer = create(tags)
108
109
    if(isFunction(callback)) {
110
        if (isString(filebuffer)) {
111
            return writeAsync(tagsBuffer, filebuffer, callback)
112
        }
113
        return callback(null, writeInBuffer(tagsBuffer, filebuffer))
114
    }
115
    if(isString(filebuffer)) {
116
        return writeSync(tagsBuffer, filebuffer)
117
    }
118
    return writeInBuffer(tagsBuffer, filebuffer)
119
}
120
121
/**
122
 * Creates a buffer containing the ID3 Tag
123
 */
124
export function create(tags: WriteTags): Buffer
125
export function create(tags: WriteTags, callback: CreateCallback): void
126
export function create(tags: WriteTags, callback?: CreateCallback) {
127
    const frames = TagsHelpers.createBufferFromTags(tags)
128
129
    //  Create ID3 header
130
    const header = Buffer.alloc(10)
131
    header.fill(0)
132
    header.write("ID3", 0)              //File identifier
133
    header.writeUInt16BE(0x0300, 3)     //Version 2.3.0  --  03 00
134
    header.writeUInt16BE(0x0000, 5)     //Flags 00
135
    ID3Util.encodeSize(frames.length).copy(header, 6)
136
137
    const id3Data = Buffer.concat([header, frames])
138
139
    if(isFunction(callback)) {
140
        return callback(id3Data)
141
    }
142
    return id3Data
143
}
144
145
function readSync(filebuffer: string | Buffer, options: Options) {
146
    if(isString(filebuffer)) {
147
        filebuffer = fs.readFileSync(filebuffer)
148
    }
149
    return TagsHelpers.getTagsFromBuffer(filebuffer, options)
150
}
151
152
function readAsync(
153
    filebuffer: string | Buffer,
154
    options: Options,
155
    callback: ReadCallback
156
) {
157
    if(isString(filebuffer)) {
158
        fs.readFile(filebuffer, (error, data) => {
159
            if(error) {
160
                callback(error, null)
161
            } else {
162
                callback(null, TagsHelpers.getTagsFromBuffer(data, options))
163
            }
164
        })
165
    } else {
166
        callback(null, TagsHelpers.getTagsFromBuffer(filebuffer, options))
167
    }
168
}
169
170
/**
171
 * Read ID3-Tags from passed buffer/filepath
172
 */
173
export function read(filebuffer: string | Buffer, options?: Options): Tags
174
export function read(filebuffer: string | Buffer, callback: ReadCallback): void
175
export function read(
176
    filebuffer: string | Buffer, options: Options, callback: ReadCallback
177
): void
178
export function read(
179
    filebuffer: string | Buffer,
180
    optionsOrCallback?: Options | ReadCallback,
181
    callback?: ReadCallback
182
): Tags | RawTags | void {
183
    const options: Options =
184
        (isFunction(optionsOrCallback) ? {} : optionsOrCallback) ?? {}
185
    callback =
186
        isFunction(optionsOrCallback) ? optionsOrCallback : callback
187
188
    if(isFunction(callback)) {
189
        return readAsync(filebuffer, options, callback)
190
    }
191
    return readSync(filebuffer, options)
192
}
193
194
/**
195
 * Update ID3-Tags from passed buffer/filepath
196
 */
197
export function update(
198
    tags: WriteTags,
199
    buffer: Buffer,
200
    options?: Options
201
): Buffer
202
export function update(
203
    tags: WriteTags,
204
    filepath: string,
205
    options?: Options
206
): true | Error
207
export function update(
208
    tags: WriteTags,
209
    filebuffer: string | Buffer,
210
    callback: WriteCallback
211
): void
212
export function update(
213
    tags: WriteTags,
214
    filebuffer: string | Buffer,
215
    options: Options,
216
    callback: WriteCallback
217
): void
218
export function update(
219
    tags: WriteTags,
220
    filebuffer: string | Buffer,
221
    optionsOrCallback?: Options | WriteCallback,
222
    callback?: WriteCallback
223
): Buffer | true | Error | void {
224
    const options: Options =
225
        (isFunction(optionsOrCallback) ? {} : optionsOrCallback) ?? {}
226
    callback =
227
        isFunction(optionsOrCallback) ? optionsOrCallback : callback
228
229
    const currentTags = read(filebuffer, options)
230
    const updatedTags = updateTags(tags, currentTags)
231
    if (isFunction(callback)) {
232
        return write(updatedTags, filebuffer, callback)
233
    }
234
    if (isString(filebuffer)) {
235
        return write(updatedTags, filebuffer)
236
    }
237
    return write(updatedTags, filebuffer)
238
}
239
240
function removeTagsSync(filepath: string) {
241
    let data
242
    try {
243
        data = fs.readFileSync(filepath)
244
    } catch(error) {
245
        return error as Error
246
    }
247
248
    const newData = removeTagsFromBuffer(data)
249
    if(!newData) {
250
        return false
251
    }
252
253
    try {
254
        fs.writeFileSync(filepath, newData, 'binary')
255
    } catch(error) {
256
        return error as Error
257
    }
258
259
    return true
260
}
261
262
function removeTagsAsync(filepath: string, callback: RemoveCallback) {
263
    fs.readFile(filepath, (error, data) => {
264
        if(error) {
265
            callback(error)
266
            return
267
        }
268
269
        const newData = removeTagsFromBuffer(data)
270
        if(!newData) {
271
            callback(error)
272
            return
273
        }
274
275
        fs.writeFile(filepath, newData, 'binary', (error) => {
276
            if(error) {
277
                callback(error)
278
            } else {
279
                callback(null)
280
            }
281
        })
282
    })
283
}
284
285
/**
286
 * Remove already written ID3-Frames from a file
287
 */
288
export function removeTags(filepath: string): boolean | Error
289
export function removeTags(filepath: string, callback: RemoveCallback): void
290
export function removeTags(filepath: string, callback?: RemoveCallback) {
291
    if(isFunction(callback)) {
292
        return removeTagsAsync(filepath, callback)
293
    }
294
    return removeTagsSync(filepath)
295
}
296
297
type Settle<T> = {
298
    (error: NodeJS.ErrnoException | Error, result: null): void
299
    (error: null, result: T): void
300
}
301
302
function makePromise<T>(callback: (settle: Settle<T>) => void) {
303
    return new Promise<T>((resolve, reject) => {
304
        callback((error, result) => {
305
            if(error) {
306
                reject(error)
307
            } else {
308
                // result can't be null here according the Settle callable
309
                // type but TS can't evaluate it properly here, so use the
310
                // null assertion, and then disable the lint error.
311
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
312
                resolve(result!)
313
            }
314
        })
315
    })
316
}
317
318
export const Promises = {
319
    create: (tags: Tags) =>
320
        makePromise((settle: Settle<Buffer>) =>
321
            create(tags, result => settle(null, result)),
322
    ),
323
    write: (tags: Tags, filebuffer: string | Buffer) =>
324
        makePromise<Buffer>((callback: WriteCallback) =>
325
            write(tags, filebuffer, callback)
326
        ),
327
    update: (tags: Tags, filebuffer: string | Buffer, options?: Options) =>
328
        makePromise<Buffer>((callback: WriteCallback) =>
329
            update(tags, filebuffer, options ?? {}, callback)
330
        ),
331
    read: (file: string, options?: Options) =>
332
        makePromise((callback: ReadCallback) =>
333
            read(file, options ?? {}, callback)
334
        ),
335
    removeTags: (filepath: string) =>
336
        makePromise((settle: Settle<void>) =>
337
            removeTags(
338
                filepath,
339
                (error) => error ? settle(error, null) : settle(null)
340
            )
341
        )
342
} as const
343
344
/**
345
 * @deprecated consider using `Promises` instead, `Promise` creates conflict
346
 *             with the Javascript native promise.
347
 */
348
export { Promises as Promise }
349